Skip to content

[FIX] Rectify: Vacuous Gate Reachability Blind Spot Bypasses Dispatch Admission Control#4162

Merged
Trecek merged 2 commits into
developfrom
is-vacuous-gate-reachability-blind-spot-bypasses-dispatch-in/4161
Jun 30, 2026
Merged

[FIX] Rectify: Vacuous Gate Reachability Blind Spot Bypasses Dispatch Admission Control#4162
Trecek merged 2 commits into
developfrom
is-vacuous-gate-reachability-blind-spot-bypasses-dispatch-in/4161

Conversation

@Trecek

@Trecek Trecek commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

_is_vacuous_gate (_recipe_composition.py:178-213) declares a capability gate step vacuous by checking whether guarded steps were pruned and whether surviving steps consume the capability ingredient — but never checks whether the gate step itself is reachable in the post-prune flow graph. Route-repair in _prune_skipped_steps can redirect upstream steps to point directly to the gate, making it reachable and executable even after all guarded steps are pruned. The result: Codex+implementation pipelines pass admission control, execute 21 minutes of side-effecting steps, then fail deterministically at the gate.

The architectural weakness is that admission-control functions (_compute_capability_feasibility, _is_vacuous_gate) operate on flat step dictionaries using property-only checks, while the recipe layer has mature BFS reachability machinery (bfs_reachable, _build_step_graph in _analysis_bfs.py) that is only consumed by semantic rules via ValidationContext — never by admission control.

The immunity approach: make graph-awareness a structural requirement for all admission-control decisions by threading the post-prune step graph through _compute_capability_feasibility and _is_vacuous_gate, and adding a reachability gate that short-circuits the vacuous determination when the gate step is unreachable.

Requirements

Investigation: Codex Pipeline DOA Bypass via _is_vacuous_gate Reachability Blind Spot

Date: 2026-06-30
Scope: Why Codex+implementation pipeline passes dispatch_infeasible admission control and runs 21 minutes of irreversible side effects before dying at gate_backend_write, despite PR #4094 and #4130 adding capability admission control.
Mode: Deep Analysis (2 exploration batches + adversarial challenge + post-report validation; 9 subagents)

Summary

The Codex+implementation pipeline bypasses admission control because _is_vacuous_gate (_recipe_composition.py:178-213, introduced by PR #4130 on June 27) incorrectly declares gate_backend_write as "vacuous" — concluding that since all guarded steps were pruned, the gate has nothing left to guard and should not trigger dispatch_infeasible. But _is_vacuous_gate only checks whether surviving steps use the capability ingredient in their with_args; it does not check whether the gate step itself remains reachable in the post-prune flow graph. After _prune_skipped_steps prunes implement, route-repair redirects create_impl_worktree.on_success from implement to gate_backend_write, making the gate directly reachable. The pipeline is dispatched, runs 11 side-effecting steps (~21 minutes), then gate_backend_write fires and routes to release_issue_failure — exactly the DOA outcome that admission control was designed to prevent.

This is the third oscillation of a pendulum pattern: PR #4094 blocks DOA pipelines → PR #4130 unblocks (vacuous-gate exemption goes too far) → today's failure. The structural cause is that admission control operates on heuristic inference (pattern-matching callables against registries) rather than author-declared intent (the purpose_critical step annotation proposed in PR #4094's REQ-SCHEMA-001, which was never implemented).

Root Cause

Primary (architectural): _is_vacuous_gate at _recipe_composition.py:178-213 has a reachability blind spot. It determines vacuity by checking two conditions: (1) at least one pre-prune step with a matching skip_when_false guard was pruned (pruned_for_capability), and (2) no surviving step (other than the gate itself) references the capability ingredient in with_args (live_uses_capability). Both conditions are met for Codex+implementation. But the function never checks whether the gate step is reachable from any surviving step via routing fields (on_success, on_failure, on_result, etc.). After route-repair, create_impl_worktree.on_success points directly to gate_backend_write. The gate is reachable, will execute, and will deterministically fail — but _is_vacuous_gate returns True. [SUPPORTED]

Proximate (why the reachability check is missing): PR #4130 was solving a real false-positive problem — #4094's admission control blocked all Codex+implementation runs even though the pruned recipe's gate was intended to be a defense-in-depth terminal. The vacuous-gate concept is architecturally correct (a gate CAN be genuinely unreachable), but the implementation checks the wrong predicate. It asks "does any surviving step consume the capability?" when it should also ask "is the gate step itself reachable from the entry point?" [SUPPORTED]

Why prior fixes did not address this: The fix chain follows a pendulum pattern:

  1. 100+ pre-[FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 fixes — each patched the step where the previous run died (downstream relocation)
  2. PR [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 (June 12) — admission control at load time (correct architecture, but over-blocked)
  3. PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130 (June 27) — vacuous-gate exemption (correct intent, but reachability gap re-opened the hole)
  4. Today (June 30) — pipeline passes admission and wastes 21 minutes before failing

The purpose_critical per-step annotation proposed in PR #4094's design (REQ-SCHEMA-001) was never implemented. The system uses inference-based heuristics layered on inference-based heuristics, each layer correcting the prior while introducing new failure surface. [SUPPORTED]

Affected Components

  • src/autoskillit/recipe/_recipe_composition.py:178-213_is_vacuous_gate(): missing reachability check [SUPPORTED]
  • src/autoskillit/recipe/_recipe_composition.py:109-175_compute_capability_feasibility(): calls _is_vacuous_gate and trusts its result [SUPPORTED]
  • src/autoskillit/recipe/_recipe_composition.py:401-524_prune_skipped_steps() (function); route-repair logic at lines 460-520 redirects create_impl_worktree.on_success to gate_backend_write [SUPPORTED]
  • src/autoskillit/recipe/_api.py:405-412load_and_validate(): calls _compute_capability_feasibility, propagates dispatch_feasible=True [SUPPORTED]
  • src/autoskillit/server/tools/tools_kitchen.py:844-847open_kitchen feasibility check: passes because dispatch_feasible=True [SUPPORTED]
  • src/autoskillit/recipes/implementation.yaml:298-365create_impl_worktree (no guard) → implement (guarded, line 328) → gate_backend_write (no guard) topology [SUPPORTED]
  • src/autoskillit/recipes/remediation.yaml:335-405 — Same topology (create_impl_worktree at 335, implement at 347, gate_backend_write at 396-405) [SUPPORTED]
  • src/autoskillit/recipes/implementation-groups.yaml:261-323 — Same topology [SUPPORTED]
  • src/autoskillit/recipe/_analysis_bfs.py:17,58bfs_reachable() and _build_step_graph() — existing reachability machinery not used by _is_vacuous_gate [SUPPORTED]
  • src/autoskillit/core/types/_type_constants.py:95-118BACKEND_CAPABILITY_INGREDIENTS, CAPABILITY_GATE_CALLABLES, CAPABILITY_INGREDIENT_TO_SKIP_GUARD registries [SUPPORTED]

Data Flow

  1. User runs AUTOSKILLIT_FEATURES__CODEX_BACKEND=true AUTOSKILLIT_AGENT_BACKEND__BACKEND=codex autoskillit order
  2. get_backend("codex")CodexBackendcapabilities.git_metadata_writable=False (codex.py:594)
  3. open_kitchen(name="implementation")_backend_capability_overrides(){"backend_supports_git_write": "false"} (_auto_overrides.py:27-28)
  4. _session_overrides.update(_backend_capability_overrides(...)) injects "false" (line 659); _promote_capability_keys(_config_layer, _session_overrides) copies it into config-authoritative layer so it wins the merge (line 661); merged at line 662
  5. load_and_validate("implementation", ingredient_overrides={"backend_supports_git_write": "false"})_prune_skipped_steps():
    • Prunes implement (line 328: skip_when_false: inputs.backend_supports_git_write)
    • Route-repair: create_impl_worktree.on_success redirected from implementgate_backend_write (lines 464-465, 484-485)
    • Prunes retry_worktree, fix, merge_gate_fix, rebase_conflict_fix, resolve_review, resolve_pre_review_conflicts — same guard
  6. _compute_capability_feasibility() evaluates surviving gate_backend_write:
    • tool=run_python ✓, callable in CAPABILITY_GATE_CALLABLES ✓, all_falsy=True ✓, on_exhausted="escalate" (schema default) ✓
    • Calls _is_vacuous_gate(): pruned_for_capability=True (implement pruned), live_uses_capability=False (no surviving step has backend_supports_git_write in with_args) → returns True
    • Gate NOT added to infeasibledispatch_feasible=True
  7. open_kitchen returns success=True → kitchen gate opens → orchestrator starts executing
  8. Side-effecting steps execute: bootstrap_clone (~3s), claim_and_resolve_issue (~3s), create_and_publish_branch (~3s), make-plan (~13min), review-approach (~5min), dry-walkthrough (~8min), create_impl_worktree (~1s)
  9. gate_backend_write(backend_supports_git_write="false"){"backend_capable": "false"} → routes to release_issue_failure
  10. Issue labeled fail, clone registered as error, worktree abandoned. ~21 minutes wasted.

Test Gap Analysis

Tests pin the bug as correct behavior — the same pattern as the June 12 investigation:

  • tests/server/test_backend_ingredient_injection.py:554-564 — asserts Codex + implementation open_kitchen returns success=True and dispatch_feasible=True (changed by PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130 from success=False, kitchen=dispatch_infeasible) [SUPPORTED]
  • tests/recipe/test_bundled_recipes_dispatch_ready.py:268-282 — function named test_codex_implementation_dispatch_infeasible but asserts dispatch_feasible is True (name-assertion mismatch from pendulum) [SUPPORTED]
  • tests/recipe/test_bundled_recipes_dispatch_ready.py:329-350test_codex_capability_gate_recipes uses disjunctive assertion accepting EITHER True OR False — cannot catch a new class of regression [SUPPORTED]
  • No end-to-end test simulates a full Codex+implementation pipeline (open_kitchen → execute steps → verify side-effect prevention). All tests are layer-local. [SUPPORTED]

Similar Patterns

  • _check_dispatch_feasibility (_preflight.py:34-114) solves the analogous problem for run_skill steps — iterates steps, checks per-step provider profile, skips steps routed to a capable backend. Same pattern needed for run_python capability gates. [SUPPORTED]
  • The recipe graph rule failure-verdict-bypass-reachable (Rectify: Failure-Verdict Bypass Immunity via Static Reachability Rule #3630) already performs terminal-classified reachability using bfs_reachable() and _build_step_graph(). The machinery for the correct check exists. [SUPPORTED]
  • The create_impl_worktree step executes at the orchestrator/host level (unsandboxed run_cmd), not inside a Codex session. All host-side git operations succeed on Codex — the irreducible gap is only implement committing inside the sandbox. [SUPPORTED]

Design Intent Findings

Historical Context

This is the third oscillation of a confirmed pendulum pattern:

The pendulum:

Fix chain (all verified by commit message + diff):
#3844#3880#3892#3902#3960#3973#3980#3981#3995#4068#4071#4075#4086#4087#4094#4130

Prior investigations directly on this topic: investigation_codex_gate_backend_write_recurrence_2026-06-12_083200.md (June 12, deep analysis, 9 subagents), investigation_codex_dispatch_infeasible_2026-06-12_151200.md (June 12, deep analysis). Both identified the root cause correctly but proposed fixes that were either not implemented as designed (purpose_critical) or introduced new gaps (vacuous-gate).

This is a recurring pattern — consider running /rectify for architectural immunity after resolving the immediate issue.

External Research

  • Codex CLI workspace-write sandbox protects .git/ as read-only by design (Linux landlock/bwrap). Headless codex exec cannot use --sandbox danger-full-access with approval_policy=never. This constraint is structural and unfixable from AutoSkillit's side. Source: openai.com/codex/concepts/sandboxing, openai/codex PR #19852. [SUPPORTED]
  • Aider performs all git operations host-side (LLM never touches .git/). AutoSkillit's static capability declaration is the most robust multi-backend pattern but is consumed only mid-pipeline, not at admission with correct reachability. [SUPPORTED]

Scope Boundary

Investigated: _is_vacuous_gate logic and its reachability blind spot; all three affected recipes (implementation, remediation, implementation-groups); _prune_skipped_steps route-repair mechanism; _compute_capability_feasibility full logic; all five admission-control enforcement surfaces (normal open_kitchen, deferred open_kitchen, get_recipe, load_recipe, dispatch_food_truck); 16-commit fix chain; installed vs. repo version parity; existing BFS reachability machinery; adversarial challenge of root cause (7 counterhypotheses tested, all refuted).

Not yet explored: Whether load_recipe soft enforcement (sets flag but doesn't early-return at tools_recipe.py:240-245) is a latent exploit path; whether purpose_critical annotation is still the correct long-term fix or whether reachability alone is sufficient; exact GitHub fail-label issue count attributable to this family; whether research/merge-prs recipes silently degrade on Codex (no gate step, so no DOA signal).

Recommendations

Single recommendation: Add post-prune reachability check to _is_vacuous_gate.

_is_vacuous_gate (_recipe_composition.py:178-213) must verify that the gate step is NOT reachable from any surviving step before declaring it vacuous. The check should use the existing _collect_all_route_targets helper (same file, line 25) which already covers all routing fields (on_success, on_failure, on_context_limit, on_rate_limit, on_exhausted, on_result conditions).

After the for cap_key in gate_input_keys loop completes (line 212 is inside the loop body), at the same indentation level as return True (line 213):

  • Scan post_prune_steps for any step (other than the gate itself) whose route targets include gate_step_name
  • If found, return False — the gate is reachable and therefore not vacuous

This leverages existing infrastructure (_collect_all_route_targets at line 25) and adds a single structural check to close the reachability gap. No schema changes, no new registries, no heuristic layers.

Complementary actions:

  1. Fix the misnamed test test_codex_implementation_dispatch_infeasible to assert dispatch_feasible=False (reversing PR [FIX] Dispatch Feasibility Immunity — Capability-Aware Admission Control #4130's flip back to [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094's correct contract)
  2. Add an end-to-end test that verifies Codex+implementation is rejected pre-claim (not just at load_and_validate level, but at open_kitchen/dispatch_food_truck level)
  3. Harden load_recipe enforcement at tools_recipe.py:240-245 from soft (flag-only) to hard (early-return refusal)

Killed alternatives:

  • Remove _is_vacuous_gate entirely — killed: re-introduces the false-positive from [FIX] Rectify: Capability Admission Control at Recipe Load — Refuse DOA Pipelines #4094 for any future recipe with genuinely unreachable gates
  • Add purpose_critical annotation (REQ-SCHEMA-001) — killed as immediate fix: correct long-term but scope is too large for the acute issue; reachability check is sufficient and composable with purpose_critical if added later
  • Add skip_when_false to create_impl_worktree — killed: create_impl_worktree correctly executes at host level (unsandboxed); blocking it blocks the worktree for non-gate uses
  • Hand-key backend_supports_git_write check in _check_dispatch_feasibility — killed: same rot class as prior fixes; does not generalize to next capability

Breakage Analysis

  • Recommendation: Add post-prune reachability check to _is_vacuous_gate
    • Breakage surface: Tests pinning dispatch_feasible=True for Codex+implementation: test_backend_ingredient_injection.py:554-564, test_bundled_recipes_dispatch_ready.py:268-282, test_capability_admission_e2e.py (vacuous-gate test). All must be updated to assert dispatch_feasible=False and infeasible_steps=["gate_backend_write"].
    • Prior reverts: None found on admission control or vacuous-gate logic (git log --grep="revert" -i)
    • Downstream contract violations: None — gate_backend_write step is retained as defense-in-depth; skip_when_false pruning unchanged; research/merge-prs recipes unaffected (no gate_backend_write step). Fleet dispatch gains earlier refusal. ingredients_only=true discovery flow is unaffected (infeasibility is on the recipe, not the ingredients).
    • Implementation sites easy to miss: The deferred-recall open_kitchen path (tools_kitchen.py:731-734) is a second enforcement site; load_recipe (tools_recipe.py:240-245) needs hard enforcement upgrade; fleet dispatch_food_truck (tools_fleet_dispatch.py:340-351) already checks dispatch_feasible.
    • Risk level: LOW — concentrated change in one function (_is_vacuous_gate), test inversions, and one soft→hard enforcement upgrade in load_recipe.

Confidence Levels

Finding Confidence
_is_vacuous_gate missing reachability check is the root cause SUPPORTED
Route-repair makes gate_backend_write reachable via create_impl_worktree SUPPORTED
All three gate recipes (implementation, remediation, implementation-groups) affected SUPPORTED
Existing BFS machinery can be reused for the fix SUPPORTED
No version skew — installed 0.10.831 == repo HEAD SUPPORTED
PR #4130 introduced the regression; PR #4094 was correct but over-blocked SUPPORTED
Pendulum pattern across the fix chain SUPPORTED
purpose_critical annotation (REQ-SCHEMA-001) never implemented SUPPORTED
Tests pin the bug as correct behavior SUPPORTED
Adversarial challenge: 7 counterhypotheses tested, all refuted SUPPORTED
load_recipe soft enforcement is a latent gap NEEDS-EVIDENCE (structural observation; no observed exploit)
Research-family recipe silent degradation on Codex NEEDS-EVIDENCE (no gate step = no DOA signal; no incident observed)

Closes #4161

Implementation Plan

Plan file: /home/talon/projects/autoskillit-runs/remediation-20260630-104324-145470/.autoskillit/temp/rectify/rectify_vacuous_gate_reachability_2026-06-30_104500.md

🤖 Generated with Claude Code via AutoSkillit

Token Usage Summary

Step Model count uncached output cache_read peak_ctx turns cache_write time
rectify* opus[1m] 1 76 28.6k 3.1M 134.0k 70 141.5k 25m 44s
review_approach* opus[1m] 1 34 6.3k 318.7k 67.6k 13 48.8k 7m 39s
dry_walkthrough* opus 1 63 16.5k 2.1M 114.9k 50 115.1k 10m 7s
implement* MiniMax-M3 1 117.2k 16.8k 4.4M 0 96 0 4m 32s
assess* opus[1m] 1 68 34.4k 4.3M 149.2k 84 127.2k 16m 34s
audit_impl* opus[1m] 1 4.9k 7.1k 418.8k 66.3k 22 44.2k 5m 54s
prepare_pr* MiniMax-M3 1 62.6k 7.4k 300.3k 0 20 0 57s
compose_pr* MiniMax-M3 1 52.3k 6.0k 230.4k 0 13 0 57s
review_pr* opus[1m] 2 81 51.3k 1.8M 96.5k 68 142.5k 14m 34s
resolve_review* opus[1m] 1 43 4.1k 918.4k 63.8k 29 41.5k 3m 43s
Total 237.4k 178.6k 17.9M 149.2k 660.7k 1h 30m

* Step used a non-Anthropic provider; caching behavior may differ.

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
rectify 0
review_approach 0
dry_walkthrough 0
implement 365 12017.3 0.0 46.0
assess 43 100372.6 2957.4 800.6
audit_impl 0
prepare_pr 0
compose_pr 0
review_pr 0
resolve_review 0
Total 408 43825.3 1619.5 437.8

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
opus[1m] 6 5.2k 131.9k 10.9M 545.6k 1h 14m
opus 1 63 16.5k 2.1M 115.1k 10m 7s
MiniMax-M3 3 232.1k 30.2k 4.9M 0 6m 27s

Trecek and others added 2 commits June 30, 2026 11:28
_is_vacuous_gate previously checked only property-level guards (pruned
steps, live capability uses) and ignored whether the gate step itself
was reachable in the post-prune flow graph. Route-repair in
_prune_skipped_steps can redirect upstream steps directly to the gate
when guarded steps are pruned, making the gate reachable and executable.

Add _gate_reachable_post_prune which uses the canonical
_analysis_graph._build_step_graph (handles on_exhausted, on_result.routes,
and skip_when_false bypass edges) plus bfs_reachable from the entry
step. _is_vacuous_gate now returns False when the gate is reachable,
forcing admission control to report dispatch_feasible=False for
Codex+implementation pipelines.

Thread post_prune_recipe as a required kwarg (no default) so any future
caller omission is a TypeError rather than a silent opt-out.

load_recipe enforcement upgraded from soft annotation to hard block:
when dispatch_feasible=False, return a failure envelope without
recipe content, matching the open_kitchen enforcement pattern.

Update tests that pinned the buggy vacuous-gate behavior as correct:
test_codex_implementation_dispatch_infeasible, test_codex_open_kitchen_*
now assert dispatch_feasible=False with gate in infeasible_steps.

Add new test_recipe_composition_vacuous_gate.py with unit tests for
_is_vacuous_gate reachable/unreachable paths and real recipe integration
tests for all three gate-equipped recipes (implementation,
remediation, implementation-groups).

Add AST invariant test verifying _compute_capability_feasibility
forwards post_prune_recipe to _is_vacuous_gate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_gate_reachable_post_prune used BFS from the entry step, which failed
when unrelated pruning (e.g. claim_and_resolve with default-condition
redirect to register_clone_failure) disconnected the entry from the
main pipeline. The gate was routable via create_impl_worktree but
unreachable from clone, causing false "vacuous" classification.

Replace with incoming-edge check via _collect_all_route_targets: a gate
with any incoming routing edge from a surviving step is considered
reachable. This is conservative (may over-reject if the routing step is
itself unreachable) but safe for admission control where false negatives
(allowing DOA pipelines) are dangerous.

Also fix _StubStep missing sub_recipe field and open_kitchen test
asserting dispatch_feasible in an envelope that doesn't include it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant